using System;
using System.Collections.Generic;
using System.Linq;
using Blackbird.Common.Exceptions;
using Blackbird.Core.Base;
using Blackbird.Core.Entities.Sprites;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Blackbird.Core
{
    public enum GameMode
    {
        Normal,
        Paused,
        Menu
    };

    [Flags]
    public enum ScreenLimit
    {
        None = 0,
        Top = 1,
        Right = 2,
        Bottom = 4,
        Left = 8
    }

    public class ScreenManager : DrawableGameComponent
    {
        // This is the main list of renderable objects.

        // This is used to temporarily store renderables that are added while proccessing collisions.
        // It's necessary to avoid problems that arise when adding to a list while iterating it.
        private readonly List<BaseRenderable> _addingQueue;
        private readonly List<BaseRenderable> _deletingQueue;

        private readonly GraphicsDeviceManager _graphicsDeviceManager;
        private readonly List<BaseRenderable> _renderables;
        private Color _backgroundColor;
        private GameMode _gameState;
        private bool _keyReleased;

        // Used to calculate the amount of mouse motion between updates.
        private int _lastMouseX;
        private int _lastMouseY;
        private int _lastScrollWheelValue;
        private long _lastTick;

        //Used to detect mouse and key events
        private bool _mouseReleased;
        private bool _processingCollisions;

        private bool _sorted;
        private SpriteBatch _spriteBatch;

        #region Events

        public event ScreenClickEvent Click;
        public event ScreenKeypressEvent Keypress;
        public event ScreenMouseMotionEvent MouseMotion;
        public event ScreenMouseWheelEvent MouseWheel;
        public event ScreenUpdateEvent ScreenUpdate;
        public event ScreenRenderEvent ScreenRender;

        #endregion

        // The cursor...
        public GameCursor Cursor { get; set; }

        public ScreenLimit ScreenLimits { get; set; }

        public GameMode GameState
        {
            get { return _gameState; }
            set { _gameState = value; }
        }

        // The background color of the screen.

        public Color BackgroundColor
        {
            get { return _backgroundColor; }
            set { _backgroundColor = value; }
        }

        public bool UsePerPixelCollisions { get; set; }

        #region Constructors

        /// <summary>
        /// Creates the main game object.
        /// </summary>
        /// <param name="game">The XNA game object</param>
        /// <param name="width">The requested width of the screen</param>
        /// <param name="height">The requested height of the screen</param>
        /// <param name="fullscreen">Selects fullscreen or windowed display mode.</param>
        /// <param name="usePerPixelCollisions">Indicates usagel of slower per-pixel collision.</param>
        public ScreenManager(Game game, int width, int height, bool fullscreen,
                             bool usePerPixelCollisions)
            : base(game)
        {
            _renderables = new List<BaseRenderable>();
            _addingQueue = new List<BaseRenderable>();
            _deletingQueue = new List<BaseRenderable>();
            _sorted = true;
            UsePerPixelCollisions = usePerPixelCollisions;
            _graphicsDeviceManager = new GraphicsDeviceManager(game) {IsFullScreen = fullscreen};
            if (!SetMode(width, height, fullscreen))
                InvalidScreenModeException.Throw();
        }

        #endregion

        private bool SetMode(int width, int height, bool fullscreen)
        {
            if (fullscreen) //Make sure the display mode is supported.
            {
                if (GraphicsAdapter.DefaultAdapter.SupportedDisplayModes.Any(displayMode => (displayMode.Width == width) && (displayMode.Height == height)))
                {
                    _graphicsDeviceManager.IsFullScreen = true;
                    _graphicsDeviceManager.PreferredBackBufferWidth = width;
                    _graphicsDeviceManager.PreferredBackBufferHeight = height;
                    _graphicsDeviceManager.ApplyChanges();
                    return true;
                }
            }
            else //Make sure the display mode fits in the window
                if ((width <= GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width) &&
                    (height <= GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height))
                {
                    _graphicsDeviceManager.IsFullScreen = false;
                    _graphicsDeviceManager.PreferredBackBufferWidth = width;
                    _graphicsDeviceManager.PreferredBackBufferHeight = height;
                    _graphicsDeviceManager.ApplyChanges();
                    return true;
                }

            return false;
        }

        /// <summary>
        /// Initializes the internal objects that the game requires.
        /// </summary>
        public override void Initialize()
        {
            //this has to be done before anything else in order to initialize the media manager
            _graphicsDeviceManager.GraphicsDevice.BlendState = BlendState.AlphaBlend;
            _lastTick = DateTime.Now.Ticks;
            _spriteBatch = new SpriteBatch(_graphicsDeviceManager.GraphicsDevice);
            _gameState = GameMode.Normal;
            _mouseReleased = true;
            _keyReleased = true;
        }

        public void Add(BaseRenderable renderable)
        {
            if (_processingCollisions)
                _addingQueue.Add(renderable);
            else
                _renderables.Add(renderable);
            _sorted = false;
        }

        public void Remove(BaseRenderable renderable)
        {
            if (_processingCollisions)
                _deletingQueue.Add(renderable);
            else
                _renderables.Remove(renderable);

            _sorted = false;
        }

        public override void Update(GameTime gameTime)
        {
            // The default timing seems to be unreliable, so a workaround is implemented using the
            // framework datetime utilities.

            long delta = DateTime.Now.Ticks - _lastTick;
            _lastTick = DateTime.Now.Ticks;

            if (ScreenUpdate != null)
            {
                var e = new ScreenUpdateEventArgs(delta);
                ScreenUpdate(this, e);
            }

            if (Cursor != null)
                Cursor.Update(delta);

            switch (GameState)
            {
                case GameMode.Normal:

                    if (!_sorted)
                        _renderables.Sort();

                    foreach (var renderable in _renderables)
                        renderable.Update(delta);

                    // Process collisions. All adds and deletes from the renderable list willed be queued until after the 
                    // processing has completed.
                    _processingCollisions = true;

                    foreach (var r1 in _renderables)
                    {
                        foreach (var r2 in _renderables)
                        {
                            if (((r1.Visible) && (r2.Visible)) &&
                                ((r1 != r2)))
                            {
                                if (!UsePerPixelCollisions)
                                {
                                    if (r1.Rectangle.Intersects(r2.Rectangle))
                                        r1.DoCollide(r2);
                                }
                                else if (r1.PixelCollide(r2))
                                    r1.DoCollide(r2);
                            }
                        }
                    }
                    _processingCollisions = false;

                    //Add everything from the adding queue
                    if (_addingQueue.Count > 0)
                        _renderables.AddRange(_addingQueue);
                    _addingQueue.Clear();

                    //Delete everything in the deletion queue...
                    if (_deletingQueue.Count > 0)
                        foreach (BaseRenderable deletedRenderable in _deletingQueue)
                            _renderables.Remove(deletedRenderable);
                    _deletingQueue.Clear();

                    break;
                case GameMode.Menu:
                    break;
                case GameMode.Paused:
                    break;
            }
            HandleInput();

            base.Update(gameTime);
        }

        private void HandleInput()
        {
            // TODO: consider manually inlining all of the input handlers.
            Keys[] keysDown = Keyboard.GetState().GetPressedKeys();

            var keys = new List<Keys>(keysDown);
            if (!_keyReleased)
                _keyReleased = keys.Count == 0;

            if (_keyReleased && (keysDown.Length > 0))
                HandleKeypress(keys);

            MouseState mouseState = Mouse.GetState();
            if (!_mouseReleased)
                _mouseReleased = (mouseState.LeftButton == ButtonState.Released) &&
                                (mouseState.MiddleButton == ButtonState.Released) &&
                                (mouseState.RightButton == ButtonState.Released);

            if (_mouseReleased && ((mouseState.LeftButton == ButtonState.Pressed) ||
                                  (mouseState.MiddleButton == ButtonState.Pressed) ||
                                  (mouseState.RightButton == ButtonState.Pressed)))
                HandleMouseClick(mouseState, keys);

            if ((_lastMouseX != mouseState.X) || (_lastMouseY != mouseState.Y))
            {
                HandleMouseMotion(mouseState, keys, mouseState.X - _lastMouseX, mouseState.Y - _lastMouseY);
                _lastMouseX = mouseState.X;
                _lastMouseY = mouseState.Y;
            }

            if (mouseState.ScrollWheelValue != _lastScrollWheelValue)
            {
                HandleMouseScrollWheel(mouseState, keys, mouseState.ScrollWheelValue - _lastScrollWheelValue);
                _lastScrollWheelValue = mouseState.ScrollWheelValue;
            }
        }

        private void HandleKeypress(List<Keys> keys)
        {
            _keyReleased = false;
            if (Keypress != null)
            {
                var e = new ScreenKeypressEventArgs(keys);
                Keypress(this, e);
            }
        }

        private void HandleMouseMotion(MouseState mouseState, List<Keys> keys, int deltaX, int deltaY)
        {
            var mousePoint = new Point(mouseState.X, mouseState.Y);
            switch (GameState)
            {
                case GameMode.Normal:
                    foreach (BaseRenderable renderable in _renderables)
                    {
                        renderable.SetMouseOver(renderable.Rectangle.Contains(mousePoint));
                        renderable.CalculateMousePosition(mouseState.X, mouseState.Y);
                    }
                    break;
                case GameMode.Paused:
                    break;
            }
            if (MouseMotion != null)
            {
                var e = new ScreenMouseMotionEventArgs(mouseState, keys, new Vector2(deltaX, deltaY));
                MouseMotion(this, e);
            }
        }

        private void HandleMouseScrollWheel(MouseState mouseState, List<Keys> keys, int wheelDelta)
        {
            if (MouseWheel != null)
            {
                var e = new ScreenMouseWheelEventArgs(mouseState, keys, wheelDelta);
                MouseWheel(this, e);
            }
        }

        private void HandleMouseClick(MouseState mouseState, List<Keys> keys)
        {
            _mouseReleased = false;
            if (Click != null)
            {
                var e = new ScreenMouseEventArgs(mouseState, keys);
                Click(this, e);
            }

            var absolutePosition = new Vector2(mouseState.X, mouseState.Y);
            switch (GameState)
            {
                case GameMode.Normal:
                    foreach (BaseRenderable renderable in ObjectsAt(absolutePosition))
                        renderable.DoClick(mouseState, keys);
                    break;
                case GameMode.Paused:
                    break;
            }
        }

        public override void Draw(GameTime gameTime)
        {
            if (ScreenRender != null)
            {
                var e = new ScreenRenderEventArgs(_spriteBatch);
                ScreenRender(this, e);
            }
            switch (_gameState)
            {
                case GameMode.Menu:
                    _graphicsDeviceManager.GraphicsDevice.Clear(Color.Blue);
                    _spriteBatch.Begin();
                    _spriteBatch.End();
                    break;
                case GameMode.Normal:
                    _graphicsDeviceManager.GraphicsDevice.Clear(_backgroundColor);
                    _spriteBatch.Begin();
                    foreach (BaseRenderable renderable in _renderables)
                        renderable.Render(_spriteBatch);
                    _spriteBatch.End();
                    break;
                case GameMode.Paused:
                    break;
            }

            if (Cursor != null)
                Cursor.Render(_spriteBatch);

            base.Draw(gameTime);
        }

        public T FindNearest<T>(BaseRenderable renderable) where T : BaseRenderable
        {
            T nearest = null;
            float minDistance = float.MaxValue;
            foreach (BaseRenderable candidate in _renderables)
            {
                if ((renderable != candidate) && (renderable.GetType() == typeof (T)))
                {
                    float distance = BaseRenderable.CalcDistance(renderable, candidate);
                    if (minDistance > distance)
                    {
                        minDistance = distance;
                        nearest = (T) candidate;
                    }
                }
            }
            return nearest;
        }

        public Texture GrabScreenShot(Rectangle area)
        {
            throw new NotImplementedException();
        }

        public Texture GrabScreenShot()
        {
            throw new NotImplementedException();
        }

        public List<BaseRenderable> ObjectsAt(float x, float y)
        {
            return ObjectsAt(new Vector2(x, y));
        }

        public List<BaseRenderable> ObjectsAt(Vector2 point)
        {
            var list = _renderables.Where(renderable => renderable.Rectangle.Contains((int) point.X, (int) point.Y)).ToList();

            list.Sort();

            return list;
        }
    }
}